#include <stdlib.h>

#include <common/macro/array.h>
#include <common/macro/build.h>
#include <common/macro/debug.h>
#include <common/util.h>

#include <query/query.h>
#include <query/filter.h>
#include <query/result.h>
#include <query/iterator.h>
#include <query/commands/insert.h>
#include <query/commands/delete.h>
#include <query/commands/update.h>
#include <storage/storage.h>
#include <storage/page.h>
#include <storage/tuple/page_tuple.h>
#include <transaction/transaction.h>

/*
 * UPDATE is a combination of INSERT and UPDATE.
 */
API_FILELOCAL result_e query_update_perform_replace(query_t *query, tuple_t *tuple_old) {
	int i;
	insert_item_t tmpitem;
	result_e result;

	tuple_old->deleted = TUPLE_UPDATE;
	tuple_old->id_delete = query->transaction->id;
	query->delete_ctx.dirty = 1;

	/*
	 * UPDATE can operate in two modes. First one is static,
	 * when we get the data before the loop executes. The other
	 * one is using callbacks to tailor data on per-column
	 * basis.
	 */
	if (query->update_ctx.funcptr == NULL) {
		/*
		 * Duplicate the current value. This is necessary, so
		 * that we can safely fill in the missing data.
		 */
		ARRAY_ALLOC(tmpitem.sizes, query->table->columns_c);
		ARRAY_ALLOC(tmpitem.data, query->table->columns_c);
		for (i = 0; i < query->table->columns_c; i++) {
			ARRAY_SET(tmpitem.sizes, i, query->insert_ctx.current.sizes[i]);
			if (query->insert_ctx.current.data[i] != NULL) {
				ARRAY_SET(tmpitem.data, i, util_memdup(query->insert_ctx.current.data[i], query->insert_ctx.current.sizes[i]));
			} else {
				ARRAY_SET(tmpitem.data, i, NULL);
			}
		}
	} else {
		debugmsg("Invoking function...\n");
		query->update_ctx.funcptr(query, query->update_ctx.ctx);
		debugmsg("DONE.\n");
		if (!query->insert_ctx.data_set)
			return RESULT_MISSINGDATA;
	}

	/* Prepare tuple. */
	tuple_init(&query->insert_ctx.current.tuple);
	query->insert_ctx.current.tuple.id_origin = tuple_old->id_origin;
	query->insert_ctx.current.tuple.id_insert = query->transaction->id;
	query->insert_ctx.current.tuple.id_command = query->transaction->command_id;
	result = query_insert_tuplify(query, &query->insert_ctx.current, &query->insert_ctx.current.tuple);
	if (result != RESULT_OK)
		return result;
	
	for (i = 0; i < query->table->columns_c; i++) {
		if (query->insert_ctx.current.tuple.atoms[i].data == NULL) {
			/*
			 * Take it from the old tuple. This preserves BLOBs.
			 */
			query->insert_ctx.current.sizes[i] = tuple_old->atoms[i].size;
			query->insert_ctx.current.data[i] = util_memdup(tuple_old->atoms[i].data, tuple_old->atoms[i].size);

			query->insert_ctx.current.tuple.atoms[i].type = tuple_old->atoms[i].type;
			query->insert_ctx.current.tuple.atoms[i].data = query->insert_ctx.current.data[i];
			query->insert_ctx.current.tuple.atoms[i].size = query->insert_ctx.current.sizes[i];
		} else {
			/*
			 * If what was here was a tuple, but is not anymore,
			 * set the delete ID of the tuple.
			 */
			if (tuple_old->atoms[i].type == TUPLE_ATOM_BLOBLINK)
				query_delete_blob(query, *(page_number_t*)tuple_old->atoms[i].data, query->transaction->id);
		}
	}
	
	/* Enqueue */
	ARRAY_EXTEND(query->insert_ctx.queue, query->insert_ctx.queue_c);
	ARRAY_SET(query->insert_ctx.queue, query->insert_ctx.queue_c - 1, query->insert_ctx.current);

	if (query->update_ctx.funcptr == NULL) {
		/* Set the current back. */
		query->insert_ctx.current = tmpitem;
		query->insert_ctx.data_set = 1;
	} else {
		query->insert_ctx.data_set = 0;
	}

	return RESULT_OK;
}

API_LIBLOCAL result_e query_update_iterator_closepage(query_t *query, page_number_t page_number, page_t *page) {
	int store_item;
	page_item_t i;
	transaction_id_t blocker_current;
	transaction_phase_e phase;
	item_pointer_t pointer_old;
	result_e result;
	page_t *page_old;
	tuple_t *tuple_old;

	if (query->delete_ctx.dirty) {
		storage_page_write(query->storage, page_number, page);
	}
	page_destroy(page);
	page = NULL;
	storage_page_rwunlock(query->storage, page_number);
	query->delete_ctx.dirty = 0;

	/*
	 * TODO: Be smarter about this. Instead of writing after each
	 * finished transaction, wait for all of them.
	 */
	while (query->delete_ctx.blockers_c > 0) {
		blocker_current = query->delete_ctx.blockers[0].blocker_id;
		debugmsg("(%d) Waiting for: %d\n", query->transaction->id, blocker_current);
		if (transaction_deleteability(query->transaction, blocker_current, &phase) != 0) {
			/* Locking failure, bail out, assume deadlock. */
			free(query->delete_ctx.blockers);
			debugmsg("DEADLOCK\n");
			return RESULT_DEADLOCK;
		}
		
		switch (phase) {
			case COMMITED:
				debugmsg("COMMITTED\n");
				/*
				 * If it was commited, add information to the list of known-deleted transactions.
				 * We should only do this if it was update, and not delete, that removed this tuple. TODO.
				 */
				store_item = 1;
				for (i = 0; i < query->update_ctx.future_deletes_c; i++) {
					if (query->update_ctx.future_deletes[i] == blocker_current) {
						store_item = 0;
						break;
					}
				}

				if (store_item) {
					ARRAY_APPEND(query->update_ctx.future_deletes, query->update_ctx.future_deletes_c, query->delete_ctx.blockers[0].blocker_id);
					query->update_ctx.need_rescan = 1;
					debugmsg("We are requesting re-scan.\n");
				} else {
					debugmsg("OMG PONNIES!!! Strange, we are closing on something unknown.\n");
					query->update_ctx.need_rescan = 1;
				}

				i = 0;
				while (i < query->delete_ctx.blockers_c) {
					if (query->delete_ctx.blockers[i].blocker_id != blocker_current) {
						i++;
						continue;
					}

					if (i < (query->delete_ctx.blockers_c - 1))
						query->delete_ctx.blockers[i] = query->delete_ctx.blockers[query->delete_ctx.blockers_c - 1];
					query->delete_ctx.blockers_c--;
				}
				break;

			default:
				debugmsg("ABORTED\n");
				/*
				 * The other transaction aborted, so we have to re-open
				 * the page and ensure that the tuples are deleted by us.
				 */
				if (storage_page_writelock(query->storage, page_number) != 0) {
					free(query->delete_ctx.blockers);
					debugmsg("DEADLOCK\n");
					return RESULT_DEADLOCK;
				}
				storage_page_read(query->storage, page_number, &page);

				i = 0;
				while (i < query->delete_ctx.blockers_c) {
					if (query->delete_ctx.blockers[i].blocker_id != blocker_current) {
						i++;
						continue;
					}

					/*
					 * Fake the pointer location.
					 */
					pointer_old = query->iterator.pointer;
					tuple_old = query->iterator.tuple;
					page_old = query->iterator.page;
					query->iterator.pointer.page_item = query->delete_ctx.blockers[i].blocker_item;
					query->iterator.pointer.page_number = page_number;
					query->iterator.page = page;
					query->iterator.tuple = &page->tuple->tuples[query->delete_ctx.blockers[i].blocker_item];

					result = query_update_perform_replace(query, &page->tuple->tuples[query->delete_ctx.blockers[i].blocker_item]);

					query->iterator.pointer = pointer_old;
					query->iterator.tuple = tuple_old;
					query->iterator.page = page_old;

					if (result != RESULT_OK)
						return result;

					if (i < (query->delete_ctx.blockers_c - 1))
						query->delete_ctx.blockers[i] = query->delete_ctx.blockers[query->delete_ctx.blockers_c - 1];
					query->delete_ctx.blockers_c--;
				}

				storage_page_write(query->storage, page_number, page);
				storage_page_rwunlock(query->storage, page_number);

				page_destroy(page);
				page = NULL;
				break;
		}
	}

	free(query->delete_ctx.blockers);
	query->delete_ctx.blockers = NULL;

	storage_page_vacuumunlock(query->storage, page_number);

	/* ...and then flush INSERT queue. */
	return query_insert_flush(query);
}

API_LIBLOCAL result_e query_update_prepare(query_t *query) {
	query->update_ctx.funcptr = NULL;
	return query_insert_prepare(query);
}

API_LIBLOCAL result_e query_update_execute(query_t *query) {
	int i, skip_item;
	delete_blocker_t blocker;
	result_e result;
	tuple_t *tuple;

	query->update_ctx.future_deletes = NULL;
	query->update_ctx.future_deletes_c = 0;

	do {
		query->update_ctx.need_rescan = 0;
		/*
		 * Not a typo -- we intentionally use DELETE openpage, but
		 * our own wrapper for closepage.
		 */
		result = query_iterator_prepare(query, query_delete_iterator_openpage, query_update_iterator_closepage);
		if (result != RESULT_OK)
			return result;
	
		result = query_filter_prepare(query);
		if (result != RESULT_OK)
			return result;
		
		/* We utilize what DELETE offers us, and rely on its context. */
		while ((result = query_iterator_step(query)) == RESULT_OK) {
			tuple = query->iterator.tuple;

			/*
			 * Visibility is a little tricky here.
			 * We care about tuples, that:
			 * a) Have origin commited AND
			 * b) Have insert commited, which may be checked by two ways:
			 *   b.1) Regular check using query_filter_tuple_visible_create OR
			 *   b.2) The tuple origin and create is not the same. 
			 */
	
			debugmsg("Transaction ID: %d.\n", query->transaction->id);
			debugmsg("HIT! Insert: %d, Delete: %d, Deleted? %d\n", tuple->id_insert, tuple->id_delete, tuple->deleted);

			/* 1. Verify visibility. */
			if (!query_filter_tuple_visible_origin(query, tuple)) {
				debugmsg("Invisible #1\n");
				continue;
			}

			if (!query_filter_tuple_visible_create(query, tuple)) {
				skip_item = 1;
				for (i = 0; i < query->update_ctx.future_deletes_c; i++) {
					if (tuple->id_insert == query->update_ctx.future_deletes[i]) {
						skip_item = 0;
						break;
					}
				}
				if (skip_item) {
					debugmsg("Invisible #2\n");
					continue;
				}
			}

			/* 2. Verify predicates. Only do it with the latest version. */
			if (!query_filter_tuple_predicates(query)) {
				debugmsg("Invisible #3\n");
				continue;
			}
			
			/* 3. Is it deleted by someone? If no, delete it. If yes, add to blockers. */
			if (tuple->deleted != TUPLE_EXISTING) {
				if (!query_filter_tuple_visible_delete(query, tuple)) {
					debugmsg("Invisible #4\n");
					continue;
				}

				skip_item = 0;
				for (i = 0; i < query->update_ctx.future_deletes_c; i++) {
					if (tuple->id_delete == query->update_ctx.future_deletes[i]) {
						skip_item = 1;
						break;
					}
				}
				if (skip_item) {
					debugmsg("Invisible #5\n");
					continue;
				}

				debugmsg("Adding as blocker\n");
				blocker.blocker_id = tuple->id_delete;
				blocker.blocker_item = query->iterator.pointer.page_item;
				ARRAY_APPEND(query->delete_ctx.blockers, query->delete_ctx.blockers_c, blocker);
				continue;
			}

			debugmsg("Processing rightaway\n");
			result = query_update_perform_replace(query, tuple);
			if (result != RESULT_OK)
				return result;
		}
		if (result_error(result))
			return result;
		
		result = query_filter_finalize(query);
		if (result != RESULT_OK)
			return result;
		
		result = query_iterator_finalize(query);
		if (result != RESULT_OK)
			return result;
	} while (query->update_ctx.need_rescan);
	debugmsg("End Of Scan\n");

	free(query->update_ctx.future_deletes);
	return result;
}

API_LIBLOCAL result_e query_update_finalize(query_t *query) {
	result_e result;
	
	result = query_insert_finalize(query);
	if (result != RESULT_OK)
		return result;

	return RESULT_OK;
}
